package com.mlming.simpledb.backend.dm.pageCache;

import com.mlming.simpledb.backend.common.AbstractCache;
import com.mlming.simpledb.backend.dm.page.Page;
import com.mlming.simpledb.backend.dm.page.PageImpl;
import com.mlming.simpledb.backend.utils.Panic;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.mlming.simpledb.common.Error;

/**
 * 通过通用的引用计数法缓存框架AbstractCache<Page> 实现的页面缓存机制
 * 用于实现DM的 读写DB文件并实现缓存
 */
public class PageCacheImpl extends AbstractCache<Page> implements PageCache {

    private static final int MEM_MIN_LIM = 10; // AbstractCache中有一个缓存最大资源数maxResource,我们规定这个必须>=10
    public static final String DB_SUFFIX = ".db"; // db文件后缀名

    private RandomAccessFile DBFile; // db文件
    private FileChannel fc; // db文件对应的通道, 用于读写操作
    private Lock fileLock; // 锁

    private AtomicInteger pageNumbers; // 记录了当前db文件有多少页。这个数字在数据库文件被打开时就会被计算，并在新建页面时自增。

    PageCacheImpl(RandomAccessFile file, FileChannel fileChannel, int maxResource) {
        super(maxResource);
        // maxResource必须 >= MEM_MIN_LIM = 10
        if(maxResource < MEM_MIN_LIM) {
            Panic.panic(Error.MemTooSmallException);
        }
        long length = 0;
        try {
            length = file.length();
        } catch (IOException e) {
            Panic.panic(e);
        }
        this.DBFile = file;
        this.fc = fileChannel;
        this.fileLock = new ReentrantLock();
        this.pageNumbers = new AtomicInteger((int)length / PAGE_SIZE); // 计算得到当前db文件中的页面数量
    }

    /**
     * 获取pgno页号对应的页 在db文件中的偏移量
     * 因为页号是从1开始的, 所以要-1
     */
    private static long pageOffset(int pgno) {
        return (pgno - 1) * PAGE_SIZE;
    }

    /**
     * 实现框架的抽象方法:
     * 根据pageNumber从数据库文件中读取页数据，并包裹成Page
     */
    @Override
    protected Page getForCache(long key) throws Exception {
        // 1) 得到页号
        int pgno = (int) key;
        // 2) 根据页号, 计算出该页在db文件中的偏移量
        long offset = pageOffset(pgno);
        // 3) 构造ByteBuffer, 用于读数据的缓冲区
        ByteBuffer buf = ByteBuffer.allocate(PAGE_SIZE);
        // 4) FileCancel定位到偏移量位置, 并把db文件对应位置读入buf中
        fileLock.lock(); // 因为要读取共享文件, 所以加锁
        try {
            fc.position(offset);
            fc.read(buf);
        } catch(IOException e) {
            Panic.panic(e);
        }
        fileLock.unlock();
        // 5) 把读取到的页面实际字节数据, 包裹成Page对象, 并返回
        return new PageImpl(pgno,buf.array(),this);
    }
    /**
     * 实现框架的抽象方法:
     * 如果page是脏页,则需要刷盘,写回db文件中
     */
    @Override
    protected void releaseForCache(Page page) {
        // 如果是脏页, 则调用flushPage方法刷盘, 既然已经写回, 则不再是脏页了
        if(page.isDirty()) {
            flushPage(page);
            page.setDirty(false);
        }
    }

    /**
     * 将指定页面刷到db文件中对应位置
     */
    @Override
    public void flushPage(Page pg) {
        int pgno = pg.getPageNumber();
        // 1) 获取该页在db文件中的偏移量
        long offset = pageOffset(pgno);
        // 2) 通过page中存储的实际字节数据, 构造ByteBuffer
        ByteBuffer buf = ByteBuffer.wrap(pg.getData());
        // 3) FileCancel定位到偏移量位置, 把buf写入db文件中
        fileLock.lock(); // 因为要操作共享文件了, 所以要加锁
        try {
            fc.position(offset);
            fc.write(buf);
            fc.force(false);
        } catch (IOException e) {
            Panic.panic(e);
        } finally {
            fileLock.unlock();
        }
    }

    /**
     * 通过传入的byte[]数组(新页的数据), 包装成一个Page页, 一般数据库也是采用先缓存然后某个时机刷盘的机制,
     * 这里我们简单点, **缓存时机等下一次调用缓存机制的get方法时才缓存，刷盘现在就刷**（这也是为了适应框架已经实现的自身逻辑）
     */
    @Override
    public int newPage(byte[] initData) {
        // 1) 获取新页号,同时PageNumbers这一代表着db文件存储的页个数的+1
        int pgno = pageNumbers.incrementAndGet();
        // 2) 包装Page页对象:
        // 注意: 这里传入的PageCache是null!! 为什么?
        // 因为逻辑上, 这个参数指的是该页面被缓存了, 可以通过pc来对其进行一些缓存相关操作
        // 这里置null代表其未被缓存, 这符合我们的逻辑,
        // 那什么时候被缓存? 或者说, 什么时候该参数被赋值
        // 当前有人需要使用该页面,通过缓存机制类的get方法获取时, 会发现缓存里没有, 就会调用上面我们实现的getForCache方法
        // 这个方法最后一行我们可以看看: return new PageImpl(pgno,buf.array(),this);
        // 此时才算缓存了, 所以该参数有值了
        Page newPage = new PageImpl(pgno,initData,null);
        // 3) 直接刷入磁盘:
        flushPage(newPage);
        // 4) 返回页号, 方便之后的读取
        return pgno;
    }

    /**
     * 从缓存中获取一个页, 此处必然是调用之前那个通用的引用计数法缓存框架
     */
    @Override
    public Page getPage(int pgno) throws Exception {
        long key = (long)pgno;
        return get(key);
    }

    // 关闭资源
    // 注意: 此处要调用框架的close方法 来对缓存资源进行一个刷盘
    @Override
    public void close() {
        super.close();
        try {
            fc.close();
            DBFile.close();
        } catch (IOException e) {
            Panic.panic(e);
        }
    }

    // 取消对该页的一个引用
    @Override
    public void release(Page page) {
        release((long)page.getPageNumber());
    }

    // 对DB文件, 从传入的参数maxPgno对应的页开始, 后面的数据全部**截断**
    @Override
    public void truncateByBgno(int maxPgno) {
        fileLock.lock(); // 因为要对文件进行操作了,
        // 1) 获取该页面 **之后** 的位置
        long size = pageOffset(maxPgno + 1);
        try {
            // 2) 截断: 那就直接把db文件的长度置为size, 则后面的都不会再被读取到
            DBFile.setLength(size);
            // 3) 把PageNumbers这个代表着db文件存储页数的值, 置为当前页号
            pageNumbers.set(maxPgno);
        } catch (IOException e) {
            Panic.panic(e);
        } finally {
            fileLock.unlock();
        }
    }

    @Override
    public int getPageNumber() {
        return pageNumbers.intValue();
    }

}
